var express = require('express');
var Excel = require("exceljs");
var path = require("path")
var fs = require("fs")
var os = require('os');
var uuid = require('uuid');
var router = express.Router();
var formidable = require('formidable');
const Instance = require('../models/Instance');
const SchemaDef = require('../models/SchemaDef');
const Category = require('../models/Category');
const Relation = require('../models/Relation');
const Tag = require('../models/Tag');
const RelationInstance = require('../models/RelationInstance');
const SchemaIcon = require('../models/SchemaIcon')
const RelationType = require('../models/RelationType')
var tools = require('../utils/tools')
var changesets = require('diff-json');
var cache = require('../utils/cache')
var kafka = require('../utils/kafka')
var kafkaProducer = kafka.producer
var settings = require('../settings')
var log = require('../utils/logs').getLogger();

var CACHE_INSTANCE = 'instance'

/**
 * @swagger
 *
 * /cmdb/instances/search:
 *   get:
 *     tags:
 *       - 实例管理
 *     summary: 全局模糊查询
 *     description: 根据编码、名字、属性值模糊查询。最多查询1000个结果，顺序随机。查询的实例按照模型分组返回，带模型图标
 *     parameters:
 *       - name: s_kw
 *         description: 关键字
 *         in: query
 *         required: false
 *         type: string
 *       - name: s_category
 *         description: 模型分类
 *         in: query
 *         required: false
 *         type: string
 *       - name: withattrs
 *         description: 关键字
 *         in: query
 *         required: false
 *         type: string
 *       - name: noicon
 *         description: 是否返回icon
 *         in: query
 *         required: false
 *         type: number
 *     responses:
 *       200:
 *         description: status/data结构
 */
router.get('/search', async function (req, res, next) {
  var keyword = req.query.s_kw || ""
  keyword = keyword.replace(/(^\s*)|(\s*$)/g, '')
  if (!keyword) {
    return res.json({ status: 'success', data: [] })
  }
  var query = getSearchQuery(keyword)
  var projection = { relations: 0, versions: 0 }
  if(req.query.withattrs !== "1") {
    projection.attrs = 0
  }

  var category = req.query.s_category || req.query.category || ""
  var nocategory = req.query.s_nocategory || req.query.nocategory || ""
  if (category) {
    var categoryList = category.split(',')
    var schemadefs = await SchemaDef.find({ category: { $in: categoryList }})
    if (schemadefs.length === 0) {
      return res.json({ status: 'success', data: [] })
    }
    query["$and"].push({ schemadef: { $in: schemadefs.map(it => it._id) }})
  } else if (nocategory) {
    var nocategoryList = nocategory.split(',')
    var schemadefs = await SchemaDef.find({ category: { $in: nocategoryList }})
    if (schemadefs.length > 0) {
      query["$and"].push({ schemadef: { $nin: schemadefs.map(it => it._id) }})
    }
  }

  var location = req.query.s_location || req.query.location || ""
  if (location) {
    var tree_data = await getSomeSchemaTreeByAttr('SM_LOCATION', 'parent', 'true')
    for(var i=0; i<tree_data.length; i++) {
      var node = iter(tree_data[i])
      if(node) {
        var locationCodeArray = []
        flat(locationCodeArray, node)
        console.log(JSON.stringify(locationCodeArray))
        query["$and"].push({
          "attrs": {
            "$elemMatch": {
              "key": "location",
              "value": {$in: locationCodeArray}
            }
          }
        })
      }
    }

    // 查找子树
    function iter(node) {
      if (node.code === location) {
        return node
      } else {
        if (node.children.length > 0) {
          for(var j=0; j<node.children.length; j++) {
            var res = iter(node.children[j])
            if (res) {
              return res
            }
          }
        }
      }
      return false
    }

    // 所有节点装入数组
    function flat(locationCodeArray, node) {
      locationCodeArray.push(node.code)
      node.children.forEach(c => {
        flat(locationCodeArray, c)
      })
    }
  }

  console.log(JSON.stringify(query))

  var populateOps = { path: 'schemadef', select: '_id code name category' }
  var instances = await Instance.find(query, projection).populate(populateOps)//.limit(1000)

  var data = instances.reduce((o, item) => {
    var { schemadef, ...other } = item._doc
    if (!o.hasOwnProperty(schemadef.code)) {
      var group = {
        schemadef: { ...schemadef._doc },
        data: [other]
      }
      o[schemadef.code] = group
    } else {
      o[schemadef.code].data.push(other)
    }
    return o
  }, {})

  var arr = Object.values(data)
  arr.forEach(it => {
    it.total = it.data.length
    //it.data = it.data.slice(0, 5)
  })

  var noicon = req.query.noicon === 0 || req.query.noicon === '0'
  if(!noicon) {
    for (var i = 0; i < arr.length; i++) {
      let icon = await SchemaIcon.findOne({ schemadef: arr[i].schemadef._id })
      arr[i].schemadef.iconUrl = icon ? icon.url : ''
    }
  }

  return res.json({ status: 'success', data: arr })
})

/**
 * @swagger
 *
 * /cmdb/instances/filter:
 *   get:
 *     tags:
 *       - 实例管理
 *     summary: 全局属性筛选
 *     description: 根据属性筛选资源
 *     responses:
 *       200:
 *         description: status/data结构
 */
router.get('/filter', async function (req, res, next) {
  console.log(req.query)
  var query = req.query
  if(!query) {
    return res.json({ status: 'success', data: [] })
  }

  var fAttrs = []
  Object.entries(query).forEach(e => {
    if(tools.isArray( e[1] )) {
      e[1].forEach(v => {
        fAttrs.push({key: e[0], value: v})
      })
    } else {
      fAttrs.push({key: e[0], value: e[1]})
    }
  })

  var instances = await Instance.find({
    attrs: {
      $elemMatch: {
        $in: fAttrs
      }
    }
  })
  .populate({ path: 'schemadef', select: '-versions' })
  .populate({ path: 'relations', populate: { path: 'src', select: '-versions' } })
  .populate({ path: 'relations', populate: { path: 'dest', select: '-versions' } })
  .populate({ path: 'relations', populate: { path: 'rela' } })

   return res.json({ status: 'success', data: instances })

})

router.get('/tpl', async function (req, res, next) {
  var schemas = await SchemaDef.aggregate([
    {
      $lookup:
      {
        from: "categories",
        localField: "category",
        foreignField: "code",
        as: "category"
      }
    }
  ]).exec()

  var workbook = new Excel.Workbook();
  schemas.forEach(s => {
    var sheet = workbook.addWorksheet(s.name);
    var columns = [{ header: '名称', key: 'name' }, { header: '唯一标识', key: 'code' }]

    if (s.category.attrs && s.category.attrs.length > 0) {
      s.category.attrs.forEach(attr => {
        columns.push({ header: attr.name, key: attr.field })
      })
    }

    if (s.attrs && s.attrs.length > 0) {
      s.attrs.forEach(attr => {
        columns.push({ header: attr.name, key: attr.field })
      })
    }
    sheet.columns = columns;
  })

  var tmpDir = settings.TMP_PATH ? settings.TMP_PATH : os.tmpdir()
  var fileName = uuid.v1() + '.xlsx'
  var tmpFile = path.join(tmpDir, fileName)
  await workbook.xlsx.writeFile(tmpFile);

  res.set({
    "Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    "Content-Disposition": "attachment; filename=" + fileName,
    "Access-Control-Expose-Headers": "Content-Disposition"
  });
  fs.createReadStream(tmpFile).pipe(res);

})

router.get('/export', async function (req, res, next) {
  var keyword = req.query.s_kw || ""
  keyword = keyword.replace(/(^\s*)|(\s*$)/g, '')
  var query = getSearchQuery(keyword)
  var results = await Instance.find(query, { relations: 0, versions: 0 }).populate({ path: 'schemadef', select: '-versions' })

  // 按模型分组
  var groups = {}
  results.map(doc => {
    groups.hasOwnProperty(doc.schemadef._id) ? groups[doc.schemadef._id].push(doc) : (groups[doc.schemadef._id] = [doc])
  })
  var workbook = new Excel.Workbook();
  groups = Object.values(groups)
  for (var i = 0; i < groups.length; i++) {
    var docs = groups[i]
    var sheet = workbook.addWorksheet(docs[0].schemadef.name, { views: [{ state: 'frozen', xSplit: 1, ySplit: 1 }] });
    var columns = [{ header: '名称', key: 'name', width: 24 }, { header: '唯一标识', key: 'code', width: 16 }]
    var category = await Category.findOne({ code: docs[0].schemadef.category })
    category.attrs.forEach(attr => {
      columns.push({ header: attr.name, key: attr.field, width: 16 })
    })
    docs[0].schemadef.attrs.forEach(attr => {
      columns.push({ header: attr.name, key: attr.field, width: 16 })
    })
    sheet.columns = columns;
    docs.forEach(doc => {
      var row = { ...doc.attrs.reduce((o, item) => { o[item.key] = item.value; return o }, {}), name: doc.name, code: doc.code }
      sheet.addRow(row);
    })

    sheet.getRow(1).alignment = { vertical: 'middle', horizontal: 'center' }
    sheet.getRow(1).font = { bold: true }
  }
  var tmpDir = settings.TMP_PATH ? settings.TMP_PATH : os.tmpdir()
  var fileName = uuid.v1() + '.xlsx'
  var tmpFile = path.join(tmpDir, fileName)
  await workbook.xlsx.writeFile(tmpFile);

  res.set({
    "Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    "Content-Disposition": "attachment; filename=" + fileName,
    "Access-Control-Expose-Headers": "Content-Disposition"
  });
  fs.createReadStream(tmpFile).pipe(res);
})

/**
 * @swagger
 *
 * /cmdb/instances/page:
 *   get:
 *     tags:
 *       - 实例管理
 *     summary: 分页查询
 *     description: 支持标准方式查询，$_foo=bar。数据量较大，慎用
 *     responses:
 *       200:
 *         description: status/data结构
 */
router.get('/page', function (req, res, next) {
  Instance.page(tools.queryResolver(req.query), req.query.page, req.query.size, result => {
    return res.json({ status: 'success', data: result.docs, pageable: result.pageable })
  })
});

function getSearchQuery (keyword) {
  if (!keyword) {
    return {}
  }
  var keywords = keyword.replace(/(\s+)/g, ' ').replace(/(^\s*)|(\s*$)/g, '').split(' ')
  var query = {
    "$and": []
  }
  keywords.forEach(kw => {
    var $or = {
      "$or": [
        { "name": new RegExp(kw, "i") },
        { "code": new RegExp(kw, "i") },
        {
          "attrs": {
            "$elemMatch": {
              "value": new RegExp(kw, "i")
            }
          }
        }]
    }
    query["$and"].push($or)
  })

  return query
}

/**
 * @swagger
 *
 * /cmdb/instances/count:
 *   get:
 *     tags:
 *       - 实例管理
 *     summary: 实例数量
 *     description:  实例数量
 *     responses:
 *       200:
 *         description: 实例数量
 */
router.get('/count', async function (req, res, next) {
  var count = await Instance.count()
  return res.json({ status: 'success', data: count })
});

/**
 * @swagger
 *
 * /cmdb/instances/count/category/{catecode}:
 *   get:
 *     tags:
 *       - 实例管理
 *     summary: 分类实例数量
 *     description:  分类实例数量
 *     parameters:
 *       - name: catecode
 *         description: 分类编码
 *         in: path
 *         required: true
 *         type: string
 *     responses:
 *       200:
 *         description: 分类实例数量
 */
router.get('/count/category/:catecode', async function (req, res, next) {
  var reqCateCodes =  req.params.catecode.split(',')
  var reqCategories = await Category.find({code: {$in: reqCateCodes } })
  if(!reqCategories) {
    return res.json({ status: 'success', data: 0 })
  }
  var categories = []
  for(var i=0; i<reqCategories.length; i++) {
    if(!reqCategories[i].parent) {
      var subCategories = await Category.find({parent: reqCategories[i].code})
      if(subCategories) {
        subCategories.forEach(it => categories.push(it))
      }
    } else {
      categories.push(reqCategories[i])
    }
  }
  
  var categoryCodes = categories.map(it => it.code)

  var schemadefs = await SchemaDef.find({category: { $in: categoryCodes } })
  var schemaIds = schemadefs.map(it => it._id)
  var count = await Instance.count({schemadef: { $in: schemaIds } })
  return res.json({ status: 'success', data: count })
});

/**
 * @swagger
 *
 * /cmdb/instances/category/{catecode}:
 *   get:
 *     tags:
 *       - 实例管理
 *     summary: 查询分类实例
 *     description:  查询分类实例
 *     parameters:
 *       - name: catecode
 *         description: 分类编码
 *         in: path
 *         required: true
 *         type: string
 *     responses:
 *       200:
 *         description: 查询分类实例
 */
router.get('/category/:catecode', async function (req, res, next) {
  var reqCateCodes =  req.params.catecode.split(',')
  var reqCategories = await Category.find({code: {$in: reqCateCodes } })
  if(!reqCategories) {
    return res.json({ status: 'success', data: 0 })
  }
  var categories = []
  for(var i=0; i<reqCategories.length; i++) {
    if(!reqCategories[i].parent) {
      var subCategories = await Category.find({parent: reqCategories[i].code})
      if(subCategories) {
        subCategories.forEach(it => categories.push(it))
      }
    } else {
      categories.push(reqCategories[i])
    }
  }
  
  var categoryCodes = categories.map(it => it.code)

  var schemadefs = await SchemaDef.find({category: { $in: categoryCodes } })
  var schemaIds = schemadefs.map(it => it._id)
  var insts = await Instance.find({schemadef: { $in: schemaIds } }, { versions: 0 }).populate(({ path: 'schemadef' }))
  return res.json({ status: 'success', data: insts })
});

/**
 * @swagger
 *
 * /cmdb/instances/category/{catecode}/foreign/{key}/{value}:
 *   get:
 *     tags:
 *       - 实例管理
 *     summary: 根据分类和外键实例
 *     description:  根据分类和外键实例
 *     parameters:
 *       - name: catecode
 *         description: 分类编码
 *         in: path
 *         required: true
 *         type: string
 *       - name: key
 *         description: 外键标识
 *         in: path
 *         required: true
 *         type: string
 *       - name: value
 *         description: 外键值
 *         in: path
 *         required: true
 *         type: string
 *     responses:
 *       200:
 *         description: 查询分类实例
 */
router.get('/category/:catecode/foreign/:key/:value', async function (req, res, next) {
  var reqCateCodes =  req.params.catecode.split(',')
  var reqCategories = await Category.find({code: {$in: reqCateCodes } })
  if(!reqCategories) {
    return res.json({ status: 'success', data: 0 })
  }
  var categories = []
  for(var i=0; i<reqCategories.length; i++) {
    if(!reqCategories[i].parent) {
      var subCategories = await Category.find({parent: reqCategories[i].code})
      if(subCategories) {
        subCategories.forEach(it => categories.push(it))
      }
    } else {
      categories.push(reqCategories[i])
    }
  }
  
  var categoryCodes = categories.map(it => it.code)

  var schemadefs = await SchemaDef.find({category: { $in: categoryCodes } })
  var schemaIds = schemadefs.map(it => it._id)

  var foreignKey = req.params.key
  var foreignValue = req.params.value
  var foreignValueArray = req.params.value.split(',')

  var insts = await Instance.find({schemadef: { $in: schemaIds }, 'attrs.key':foreignKey,'attrs.value': {$in: foreignValueArray} }, { versions: 0 }).populate(({ path: 'schemadef' }))
  return res.json({ status: 'success', data: insts })
});

/**
 * @swagger
 *
 * /cmdb/instances/schema/{schemadef}:
 *   get:
 *     tags:
 *       - 实例管理
 *     summary: 模型实例（不分页）
 *     description: 根据编码、名字、属性值模糊查询。支持根据属性key:value精确查询s_foo=bar
 *     parameters:
 *       - name: schemadef
 *         description: 模型ID或编码
 *         in: path
 *         required: true
 *         type: string
 *       - name: s_kw
 *         description: 关键字
 *         in: query
 *         required: false
 *         type: string
 *     responses:
 *       200:
 *         description: status/data结构
 */
router.get('/schema/:schemadef', async function (req, res, next) {

  var isById = tools.isValidObjectId(req.params.schemadef)
  var schemaQuery = isById ? { _id: req.params.schemadef } : { code: req.params.schemadef }
  var schemaDef = await SchemaDef.findOne(schemaQuery)
  if (!schemaDef) {
    return tools.handleError(res, '模型不存在')
  }

  var keyword = req.query.s_kw || ""
  keyword = keyword.replace(/(^\s*)|(\s*$)/g, '')
  var query = getSearchQuery(keyword)
  query.schemadef = schemaDef._id
  // 支持根据属性key:value精确查询
  Object.keys(req.query).filter(key => key !== 's_kw' && key.indexOf('s_') > -1).forEach(queryKey => {
    query.attrs = { "$elemMatch": { "key": queryKey.substring(2), value: req.query[queryKey] } }
  })

  Instance.find(query, { versions: 0 })
    .populate({ path: 'schemadef', select: '-versions' })
    .exec((err, docs) => {
      if (err) return tools.handleError(res, err);
      return res.json({ status: 'success', data: docs })
    })
});

/**
 * @swagger
 *
 * /cmdb/instances/schema/{schemadef}/tree/by/{attr}:
 *   get:
 *     tags:
 *       - 实例管理
 *     summary: 属性实例树
 *     description: 构造特定属性的树形结构数据。如，位置树是根据位置模型实例的parent属性构造。API层缓存1分钟
 *     parameters:
 *       - name: schemadef
 *         description: 模型ID或编码
 *         in: path
 *         required: true
 *         type: string
 *       - name: attr
 *         description: 属性编码
 *         in: path
 *         required: true
 *         type: string
 *       - name: simple
 *         description: 简单版
 *         in: query
 *         required: false
 *         type: boolean
 *     responses:
 *       200:
 *         description: status/data结构
 */
router.get('/schema/:schemadef/tree/by/:attr', async function (req, res, next) {

  try {
    var instances = await getSomeSchemaTreeByAttr(req.params.schemadef, req.params.attr, req.query.simple)
  } catch (err) {
    return tools.handleError(res, err)
  }
  return res.json({ status: 'success', data: instances })

});

async function getSomeSchemaTreeByAttr(schemadef, attr, isSimple) {

  var isById = tools.isValidObjectId(schemadef)
  var schemaQuery = isById ? { _id: schemadef } : { code: schemadef }
  var schemaDef = await SchemaDef.findOne(schemaQuery)
  if (!schemaDef) {
    throw '模型不存在'
  }

  var query = { schemadef: schemaDef._id }
  var byAttr = attr

  var cached = await cache.exists(CACHE_INSTANCE, schemaDef.code + "::" + byAttr + '::tree')
  if(!cached) {
    var docs = await Instance.find({ ...query, "attrs": { "$elemMatch": { "key": byAttr, $or: [{ value: { $exists: false } }, { value: null }, { value: '' }] } } }, { _id: 1, code: 1, name: 1, attrs:1 })
    var instances = docs.map(it => it._doc)
    for (var i = 0; i < instances.length; i++) {
      instances[i].children = []
      await getChildren(query, byAttr, instances[i])
    }
    // 缓存设置60秒
    cache.set(CACHE_INSTANCE, schemaDef.code + "::" + byAttr + '::tree', instances, 60 * 1000)
  }
  var instances = await cache.get(CACHE_INSTANCE, schemaDef.code + "::" + byAttr + '::tree')
  if(isSimple === 'true') {
    instances.forEach(inst => {
      if(inst.attrs) {
        delete inst.attrs
      }
      if(inst.children) {
        clearAttrs(inst.children)
      }
    })

    function clearAttrs(children) {
      children.forEach(inst => {
        if(inst.attrs) {
          delete inst.attrs
        }
        if(inst.children) {
          clearAttrs(inst.children)
        }
      })
    }
  }

  return instances
}

async function getChildren (query, byAttr, parent) {
  var docs = await Instance.find({ ...query, "attrs": { "$elemMatch": { "key": byAttr, "value": parent.code } } }, { _id: 1, code: 1, name: 1, attrs:1 })
  var instances = docs.map(it => it._doc)
  for (var i = 0; i < instances.length; i++) {
    instances[i].children = []
    await getChildren(query, byAttr, instances[i])
    parent.children.push(instances[i])
  }
}

/**
 * @swagger
 *
 * /cmdb/instances/foreign/{key}/{value}:
 *   get:
 *     tags:
 *       - 实例管理
 *     summary: 查询外键实例
 *     description:  查询外键实例
 *     parameters:
 *       - name: key
 *         description: 外键标识
 *         in: path
 *         required: true
 *         type: string
 *       - name: value
 *         description: 外键值
 *         in: path
 *         required: true
 *         type: string
 *     responses:
 *       200:
 *         description: 查询外键实例
 */
router.get('/foreign/:key/:value', async function (req, res, next) {
  var foreignKey = req.params.key
  var foreignValue = req.params.value
  var insts = await Instance.find( {'attrs.key':foreignKey,'attrs.value':foreignValue} ).populate(({ path: 'schemadef' }))
  return res.json({ status: 'success', data: insts })
})

/**
 * @swagger
 *
 * /cmdb/instances/schema/{schemadef}/page:
 *   get:
 *     tags:
 *       - 实例管理
 *     summary: 分页查询模型实例
 *     description: 根据编码、名字、属性值模糊查询。支持根据属性key:value精确查询s_foo=bar
 *     parameters:
 *       - name: schemadef
 *         description: 模型ID或编码
 *         in: path
 *         required: true
 *         type: string
 *       - name: s_kw
 *         description: 关键字
 *         in: query
 *         required: false
 *         type: string
 *     responses:
 *       200:
 *         description: status/data结构
 */
router.get('/schema/:schemadef/page', async function (req, res, next) {

  var isById = tools.isValidObjectId(req.params.schemadef)
  var schemaQuery = isById ? { _id: req.params.schemadef } : { code: req.params.schemadef }
  var schemaDef = await SchemaDef.findOne(schemaQuery)
  if (!schemaDef) {
    return tools.handleError(res, '模型不存在')
  }

  var keyword = req.query.s_kw || ""
  keyword = keyword.replace(/(^\s*)|(\s*$)/g, '')
  var query = getSearchQuery(keyword)
  query.schemadef = schemaDef._id
  // 支持根据属性key:value精确查询
  Object.keys(req.query).filter(key => key !== 's_kw' && key.indexOf('s_') > -1).forEach(queryKey => {
    query.attrs = { "$elemMatch": { "key": queryKey.substring(2), value: req.query[queryKey] } }
  })

  Instance.page(query, req.query.page, req.query.size, result => {
    return res.json({ status: 'success', data: result.docs, pageable: result.pageable })
  })
});

/**
*  指定模型分页查询模型实例
*/
router.get('/schema/:schemadef/export', async function (req, res, next) {

  var isById = tools.isValidObjectId(req.params.schemadef)
  var schemaQuery = isById ? { _id: req.params.schemadef } : { code: req.params.schemadef }
  var schemaDef = await SchemaDef.findOne(schemaQuery)
  if (!schemaDef) {
    return tools.handleError(res, '模型不存在')
  }

  var keyword = req.query.s_kw || ""
  keyword = keyword.replace(/(^\s*)|(\s*$)/g, '')
  const query = getSearchQuery(keyword)
  query.schemadef = schemaDef._id

  var results = await Instance.find(query, { relations: 0, versions: 0 })
  var category = await Category.findOne({ code: schemaDef.category })

  var workbook = new Excel.Workbook();
  var sheet = workbook.addWorksheet(schemaDef.name);
  var columns = [{ header: '名称', key: 'name', width: 24 }, { header: '唯一标识', key: 'code', width: 16 }]

  var allAttrs = [...category.attrs, ...schemaDef.attrs]
  allAttrs.forEach(attr => {
    columns.push({ header: attr.name, key: attr.field, width: 16 })
  })

  sheet.columns = columns;
  results.forEach(doc => {
    var row = { ...doc.attrs.reduce((o, item) => { o[item.key] = item.value; return o }, {}), name: doc.name, code: doc.code }
    sheet.addRow(row);
  })
  sheet.getRow(1).alignment = { vertical: 'middle', horizontal: 'center' }
  sheet.getRow(1).font = { bold: true }

  var tmpDir = settings.TMP_PATH ? settings.TMP_PATH : os.tmpdir()
  var fileName = uuid.v1() + '.xlsx'
  var tmpFile = path.join(tmpDir, fileName)
  await workbook.xlsx.writeFile(tmpFile);

  res.set({
    "Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    "Content-Disposition": "attachment; filename=" + fileName,
    "Access-Control-Expose-Headers": "Content-Disposition"
  });
  fs.createReadStream(tmpFile).pipe(res);
});

/**
* 导入资源实例
*/
router.post('/schema/:schemadef/import', async function (req, res, next) {
  let form = new formidable.IncomingForm();
  form.encoding = 'utf-8';
  form.keepExtensions = true;

  form.parse(req, async (err, fields, files) => {
    if (err) return tools.handleError(res, err);
    const filePath = files.file.path;
    const workbook = new Excel.Workbook();
    await workbook.xlsx.readFile(filePath);

    var schemaDef = await SchemaDef.findById(req.params.schemadef)
    var category = await Category.findOne({ code: schemaDef.category })
    var allAttrs = [...category.attrs, ...schemaDef.attrs]

    const worksheet = workbook.getWorksheet(schemaDef.name);
    const row = worksheet.getRow(1);

    // [ [列序号,列名称] ]
    const colSeq = []
    row.eachCell(function (cell, colNumber) {
      colSeq.push([colNumber, cell.value])
    });


    // const attrNames = colSeq.filter(s => s[1] !== '名称' && s[1] !== '唯一标识')
    const mAttrs = allAttrs.filter(it => colSeq.some(n => it.name === n[1]))
    const attrMap = {}
    mAttrs.forEach(it => {
      attrMap[it.name] = it
    })

    // 遍历工作表中具有值的所有行
    const insts = []
    worksheet.eachRow(function (row, rowNumber) {
      if (rowNumber > 1) {
        const inst = {
          schemadef: schemaDef._id,
          code: '',
          name: '',
          desc: ''
        }
        const codeCol = colSeq.find(it => it[1] === '唯一标识')
        if (codeCol) {
          inst.code = row.getCell(codeCol[0]).value
        }
        const nameCol = colSeq.find(it => it[1] === '名称')
        if (nameCol) {
          inst.name = row.getCell(nameCol[0]).value
        }
        const descCol = colSeq.find(it => it[1] === '描述')
        if (descCol) {
          inst.desc = row.getCell(descCol[0]).value
        }

        const instAttrs = []
        colSeq.forEach(s => {
          if (attrMap[s[1]]) {
            let val = row.getCell(s[0]).value
            if (!!val && (attrMap[s[1]].dtype === 'foreign' || attrMap[s[1]].dtype === 'enum' || attrMap[s[1]].dtype === 'datasource' || attrMap[s[1]].dtype === 'bool')) val = val.split('$$')[0]
            instAttrs.push({
              key: attrMap[s[1]].field,
              value: val
            })
          }
        })
        inst.attrs = instAttrs
        insts.push(inst)
      }
    });

    try {
      var docs = await Instance.insertMany(insts, { ordered: false, rawResult: true })
      return res.json({ status: 'success', data: docs.length })
    } catch (err) {
      log.error(err)
      return tools.handleError(res, err.writeErrors);
    }
  })
})

/**
 * @swagger
 *
 * /cmdb/instances:
 *   post:
 *     tags:
 *       - 实例管理
 *     summary: 创建实例
 *     description: 创建实例。属性按照{key1冒号value1, key2冒号value2,...}的形式提交
 *     parameters:
 *       - name: schemadef
 *         description: 模型ID
 *         in: formData
 *         required: true
 *         type: string
 *       - name: code
 *         description: 实例编码
 *         in: formData
 *         required: true
 *         type: string
 *       - name: name
 *         description: 实例名称
 *         in: formData
 *         required: true
 *         type: string
 *       - name: desc
 *         description: 实例描述
 *         in: formData
 *         required: false
 *         type: string
 *     responses:
 *       200:
 *         description: status/data结构
 */
router.post('/', async function (req, res, next) {
  var isById = tools.isValidObjectId(req.body.schemadef)
  var schemaQuery = isById ? { _id: req.body.schemadef } : { code: req.body.schemadef }
  var schemaDef = await SchemaDef.findOne(schemaQuery)

  if (!schemaDef) return tools.handleError(res, '模型不存在');
  // 通过模型的属性定义，筛选传递的参数
  var attrs = []
  var category = await Category.findOne({ code: schemaDef.category })

  category.attrs.forEach(a => {
    attrs.push({ key: a.field, value: req.body[a.field] })
  })

  schemaDef.attrs.forEach(a => {
    attrs.push({ key: a.field, value: req.body[a.field] })
  })

  var { code, name, desc } = req.body
  Instance.create({ name, desc, code, schemadef: schemaDef._id, attrs }, function (err, doc) {
    if (err) {
      if (err.message.indexOf('duplicate key error') !== -1) {
        var msg = '唯一性约束错误'
        return tools.handleError(res, msg)
      }
      return tools.handleError(res, err)
    };
    return res.json({ status: 'success', data: doc })
  });
});

/**
 * @swagger
 *
 * /cmdb/instances/{id}:
 *   get:
 *     tags:
 *       - 实例管理
 *     summary: 实例详情
 *     description: 实例详情。查询参数attrs可以使单个属性，数组，或逗号分隔。查询参数tags可以使单个属性，数组，或逗号分隔
 *     parameters:
 *       - name: id
 *         description: 实例ID或编码
 *         in: path
 *         required: true
 *         type: string
 *       - name: attrs
 *         description: 属性编码
 *         in: query
 *         required: false
 *         type: string
 *       - name: tags
 *         description: 标签编码
 *         in: query
 *         required: false
 *         type: string
 *     responses:
 *       200:
 *         description: status/data结构
 */
router.get('/:id', async function (req, res, next) {
  // 查询指定属性的属性值
  var attrs = req.query.attrs || []
  if (!tools.isArray(attrs)) {
    attrs = attrs.split(',');
  }
  // 查询指定Tag的属性值
  var tags = req.query.tags || []
  if (!tools.isArray(tags)) {
    tags = tags.split(',');
  }

  var cached = await cache.exists(CACHE_INSTANCE, req.params.id)
  if (cached) {
    var doc = await cache.get(CACHE_INSTANCE, req.params.id)
    if (tags.length > 0) {
      var schemadef = doc.schemadef._id
      var tagModels = await Tag.find({ schemadef, code: { $in: tags } })
      var tagAttrs = new Set()
      tagModels.forEach(m => {
        m.attrs.forEach(a => tagAttrs.add(a))
      })
      attrs = new Set([...attrs, ...tagAttrs])
      attrs = [...attrs]
    }
    if (attrs.length > 0) {
      doc.attrs = doc.attrs.filter(it => attrs.includes(it.key))
    }
    return res.json({ status: 'success', data: doc })
  } else {
    var isById = tools.isValidObjectId(req.params.id)
    var query = isById ? { _id: req.params.id } : { code: req.params.id }
    var match = isById ? { _id: { $ne: req.params.id } } : { code: { $ne: req.params.id } }
    Instance.findOne(query)
      .populate({ path: 'schemadef', select: '-versions' })
      .populate({ path: 'relations', populate: { path: 'src', match: match, select: '-versions' } })
      .populate({ path: 'relations', populate: { path: 'dest', match: match, select: '-versions' } })
      .populate({ path: 'relations', populate: { path: 'rela' } })
      .exec(async (err, doc) => {
        if (err) return tools.handleError(res, err);
        if (!doc) return tools.handleError(res, '实例不存在');

        var category = await Category.findOne({ code: doc.schemadef.category })
        // 把分类属性和模型属性合并到模型属性中
        doc.schemadef.attrs = [...doc.schemadef.attrs, ...category.attrs]
        if (!cached) {
          cache.set(CACHE_INSTANCE, req.params.id, doc, 6 * 1000)
        }
        if (tags.length > 0) {
          var schemadef = doc.schemadef._id
          var tagModels = await Tag.find({ schemadef, code: { $in: tags } })
          var tagAttrs = new Set()
          tagModels.forEach(m => {
            m.attrs.forEach(a => tagAttrs.add(a))
          })
          attrs = new Set([...attrs, ...tagAttrs])
          attrs = [...attrs]
        }
        if (attrs.length > 0) {
          doc.attrs = doc.attrs.filter(it => attrs.includes(it.key))
        }
        return res.json({ status: 'success', data: doc })
      })
  }
});

/**
 * @swagger
 *
 * /cmdb/instances/{id}:
 *   put:
 *     tags:
 *       - 实例管理
 *     summary: 更新实例
 *     description: 实例详情。属性按照{key1冒号value1, key2冒号value2,...}的形式提交。属性更新是全量更新，否则已存在的属性值会被抹掉，慎用。
 *     parameters:
 *       - name: id
 *         description: 实例ID或编码
 *         in: path
 *         required: true
 *         type: string
 *       - name: name
 *         description: 名字
 *         in: formData
 *         required: true
 *         type: string
 *       - name: desc
 *         description: 描述
 *         in: formData
 *         required: false
 *         type: string
 *     responses:
 *       200:
 *         description: status/data结构
 */
router.put('/:id', async function (req, res, next) {

  var isById = tools.isValidObjectId(req.params.id)
  var query = isById ? { _id: req.params.id } : { code: req.params.id }
  var beforeUpdate = await Instance.findOne(query).populate({ path: 'schemadef', select: '-versions' })
  if (!beforeUpdate) {
    return tools.handleError(res, '实例不存在')
  }

  var attrs = []
  var category = await Category.findOne({ code: beforeUpdate.schemadef.category })
  category.attrs.forEach(a => {
    attrs.push({ key: a.field, value: req.body[a.field] })
  })
  beforeUpdate.schemadef.attrs.forEach(a => {
    attrs.push({ key: a.field, value: req.body[a.field] })
  })

  var { name, desc } = req.body
  var data = {
    name,
    desc,
    attrs,
    updated: Date.now()
  }

  var afterUpdate = await Instance.findOneAndUpdate(query, { $set: data }, { new: true });
  // 移除缓存. 缓存内可能以ID和CODE两种方式缓存
  cache.remove(CACHE_INSTANCE, [afterUpdate._id, afterUpdate.code])

  // 计算变更项
  var diffs = changesets.diff(beforeUpdate.attrs.reduce((o, item) => { o.key = item.value; return o }, {}), afterUpdate.attrs.reduce((o, item) => { o[item.key] = item.value; return o }, {}));
  if (diffs.length > 0) {
    // 记录变更人
    if (req.user.is_authenticated) {
      diffs.forEach(d => {
        d.operator = req.user.username
        d.operatorName = req.user.name
      })
    }
    // 资产变更发送到kafka
    kafkaProducer.send([
      {
        topic: settings.KAFKA.ASSET_CHANGE_TOPIC,
        messages: JSON.stringify({ code: afterUpdate.code, diffs: diffs })
      },
      {
        topic: settings.KAFKA.LOG_TOPIC,
        messages: JSON.stringify({
          message: JSON.stringify({
            app: 'cmdb',
            ref: afterUpdate.code,
            account: req.user ? req.user.username : '',
            username: req.user ? req.user.name : '',
            ip: req.ip,
            operation: 'CI项变更',
            api: req.originalUrl,
            extra: JSON.stringify(diffs),
            time: new Date()
          }),
          sendDate: new Date()
        })
      }
    ], (err, result) => {
      log.debug(err, result)
    })
    Instance.findOneAndUpdate(query, { $push: { versions: { updated: afterUpdate.updated, diffs } } }, { new: true }, function (err, doc) {
      if (err) return tools.handleError(res, err);
      return res.json({ status: 'success', data: afterUpdate })
    });
  } else {
    return res.json({ status: 'success', data: afterUpdate })
  }
});

/**
 * @swagger
 *
 * /cmdb/instances/{id}/attrs:
 *   put:
 *     tags:
 *       - 实例管理
 *     summary: 批量更新实例属性值
 *     description: 批量更新实例属性值
 *     parameters:
 *       - name: id
 *         description: 实例ID或编码
 *         in: path
 *         required: true
 *         type: string
 *       - name: attrs
 *         description: 属性键值对
 *         in: formData
 *         required: true
 *         type: list
 *     responses:
 *       200:
 *         description: status/data结构
 */
router.put('/:id/attrs', async function (req, res, next) {
  var isById = tools.isValidObjectId(req.params.id)
  var query = isById ? { _id: req.params.id } : { code: req.params.id }
  var beforeUpdate = await Instance.findOne(query).populate({ path: 'schemadef', select: '-versions' })
  if (!beforeUpdate) {
    return tools.handleError(res, '实例不存在')
  }

  var uAttrs = req.body.attrs
  var afterUpdate = beforeUpdate
  var queryWithAttr = { ...query }
  for(var i = 0; i < uAttrs.length; i++) {
    queryWithAttr["attrs.key"] = uAttrs[i].key
    var data = {
      updated: Date.now(),
      "attrs.$.value": uAttrs[i].value
    }
    afterUpdate = await Instance.findOneAndUpdate(queryWithAttr, { $set: data }, { new: false });
  }
  
  // 清空缓存
  cache.remove(CACHE_INSTANCE, [beforeUpdate._id, beforeUpdate.code])

  // var { versions, ...other } = afterUpdate._doc
  // return res.json({ status: 'success', data: other })

  Instance.findOne(query, (err, doc) => {
    if (err) return tools.handleError(res, err);
    return res.json({ status: 'success', data: doc })
  })
});

/**
 * @swagger
 *
 * /cmdb/instances/{id}/{attr}:
 *   put:
 *     tags:
 *       - 实例管理
 *     summary: 更新实例属性值
 *     description: 单个属性更新
 *     parameters:
 *       - name: id
 *         description: 实例ID或编码
 *         in: path
 *         required: true
 *         type: string
 *       - name: attr
 *         description: 属性编码
 *         in: path
 *         required: true
 *         type: string
 *       - name: value
 *         description: 属性值
 *         in: formData
 *         required: true
 *         type: string
 *     responses:
 *       200:
 *         description: status/data结构
 */
router.put('/:id/:attr', async function (req, res, next) {

  var isById = tools.isValidObjectId(req.params.id)
  var query = isById ? { _id: req.params.id } : { code: req.params.id }
  var beforeUpdate = await Instance.findOne(query).populate({ path: 'schemadef', select: '-versions' })
  if (!beforeUpdate) {
    return tools.handleError(res, '实例不存在')
  }

  var category = await Category.findOne({ code: beforeUpdate.schemadef.category })
  var allAttrs = [...category.attrs, ...beforeUpdate.schemadef.attrs]

  var forUpdateAttr = allAttrs.find(attr => attr.field === req.params.attr)
  if(!forUpdateAttr) {
    return tools.handleError(res, '属性不存在');
  }

  var data = {
    updated: Date.now(),
    "attrs.$.value": req.body.value
  }
  query["attrs.key"] = req.params.attr

  var afterUpdate = await Instance.findOneAndUpdate(query, { $set: data }, { new: true });
  // 清空缓存
  cache.remove(CACHE_INSTANCE, [beforeUpdate._id, beforeUpdate.code])
  var diffs = changesets.diff(beforeUpdate.attrs.reduce((o, item) => { o[item.key] = item.value; return o }, {}), afterUpdate.attrs.reduce((o, item) => { o[item.key] = item.value; return o }, {}));
  if (diffs.length > 0) {
    if (req.user.is_authenticated) {
      diffs.forEach(d => {
        d.operator = req.user.username
        d.operatorName = req.user.name
      })
    }
    kafkaProducer.send([
      {
        topic: settings.KAFKA.ASSET_CHANGE_TOPIC,
        messages: JSON.stringify({ code: afterUpdate.code, diffs: diffs })
      },
      {
        topic: settings.KAFKA.LOG_TOPIC,
        messages: JSON.stringify({
          message: JSON.stringify({
            app: 'cmdb',
            ref: afterUpdate.code,
            account: req.user ? req.user.username : '',
            username: req.user ? req.user.name : '',
            ip: req.ip,
            operation: 'CI项变更',
            api: req.originalUrl,
            extra: JSON.stringify(diffs),
            time: new Date()
          }),
          sendDate: new Date()
        })
      }
    ], (err, result) => {
      if (err) {
        log.error(err)
      }
      log.debug('变更项发往KAFKA结果：', result)
    })
    Instance.findOneAndUpdate(query, { $push: { versions: { updated: afterUpdate.updated, diffs } } }, { new: true }, function (err, doc) {
      if (err) {
        log.error(err)
        return tools.handleError(res, err);
      }
      var { versions, ...other } = doc._doc
      return res.json({ status: 'success', data: other })
    });
  } else {
    var { versions, ...other } = afterUpdate._doc
    return res.json({ status: 'success', data: other })
  }

});

/**
 * @swagger
 *
 * /cmdb/instances/batch/{schemadef}/{attr}:
 *   put:
 *     tags:
 *       - 实例管理
 *     summary: 批量更新实例属性值
 *     description: 批量更新实例属性值
 *     parameters:
 *       - name: schemadef
 *         description: 模型ID
 *         in: path
 *         required: true
 *         type: string
 *       - name: attr
 *         description: 属性编码
 *         in: path
 *         required: true
 *         type: string
 *       - name: value
 *         description: 属性值
 *         in: formData
 *         required: true
 *         type: string
 *     responses:
 *       200:
 *         description: status/data结构
 */
router.put('/batch/:schemadef/:attr', async function (req, res, next) {
  var query = {
    schemadef: req.params.schemadef,
    "attrs.key": req.params.attr
  }
  var data = {
    updated: Date.now(),
    "attrs.$.value": req.body.value
  }

  var result = await Instance.updateMany(query, { $set: data })

  var nModified = result.nModified

  var query = {
    schemadef: req.params.schemadef,
    "attrs.key": {$ne: req.params.attr}
  }

  var result = await Instance.updateMany(query, { $push: { attrs: { key: req.params.attr, value: req.body.value } } })
  nModified += result.nModified

  return res.json({ status: 'success', data: { nModified } })
});

/**
 * @swagger
 *
 * /cmdb/instances/batch:
 *   delete:
 *     tags:
 *       - 实例管理
 *     summary: 批量删除实例
 *     description: 批量删除实例，只支持根据ID删除
 *     parameters:
 *       - name: ids
 *         description: id数组
 *         in: formData
 *         required: true
 *         type: list
 *     responses:
 *       200:
 *         description: status/data结构
 */
router.delete('/batch', async function (req, res, next) {
  var allInstance = await Instance.find({_id: {$in: req.body.ids} })
  for (var i=0; i<allInstance.length; i++) {
    var inst = allInstance[i]
    // 先查出所有的关系实例
    var allRelationInstance = await RelationInstance.find({ $or: [{ src: inst._id }, { dest: inst._id }] })
    var allRelationInstanceId = allRelationInstance.map(it => it._id)
    // 先删除所有的关系实例
    await RelationInstance.deleteMany({_id: {$in: allRelationInstanceId}})
    // 删除所有相关实例对关系的引用
    await Instance.updateMany({ relations: { $in: allRelationInstanceId } }, { $pull: { relations: { $in: allRelationInstanceId } } })
    await Instance.findOneAndDelete({_id: inst._id})
  }
  return res.json({ status: 'success', data: allInstance.length})
})

/**
 * @swagger
 *
 * /cmdb/instances/{id}:
 *   delete:
 *     tags:
 *       - 实例管理
 *     summary: 删除实例
 *     description: 相关的关系同时会被删除
 *     parameters:
 *       - name: id
 *         description: 实例ID
 *         in: path
 *         required: true
 *         type: string
 *     responses:
 *       200:
 *         description: status/data结构
 */
router.delete('/:id', async function (req, res, next) {
  var isById = tools.isValidObjectId(req.params.id)
  var query = isById ? { _id: req.params.id } : { code: req.params.id }
  var inst = await Instance.findOne(query)
  if(!inst) {
    return tools.handleError(res, '实例不存在');
  }
  // 先查出所有的关系实例
  var allRelationInstance = await RelationInstance.find({ $or: [{ src: inst._id }, { dest: inst._id }] })
  var allRelationInstanceId = allRelationInstance.map(it => it._id)
  // 先删除所有的关系实例
  await RelationInstance.deleteMany({_id: {$in: allRelationInstanceId}})
  // 删除所有相关实例对关系的引用
  await Instance.updateMany({ relations: { $in: allRelationInstanceId } }, { $pull: { relations: { $in: allRelationInstanceId } } })
  // 删除当前实例
  Instance.findOneAndDelete(query, function (err) {
    if (err) return tools.handleError(res, err);
    return res.json({ status: 'success' })
  });
});

/**
 * @swagger
 *
 * /cmdb/instances/{id}/tags:
 *   get:
 *     tags:
 *       - 实例管理
 *     summary: 查询资源标签属性信息
 *     description: 数据比较纯净。tags=basic,3d或tags=basic&tags=3d。不传tags参数时，默认返回所有已定义标签的信息
 *     parameters:
 *       - name: id
 *         description: 实例ID或编码
 *         in: path
 *         required: true
 *         type: string
 *       - name: tags
 *         description: 标签
 *         in: query
 *         required: false
 *         type: string
 *     responses:
 *       200:
 *         description: status/data结构
 */
router.get('/:id/tags', async function (req, res, next) {
  var isById = tools.isValidObjectId(req.params.id)
  var query = isById ? { _id: req.params.id } : { code: req.params.id }

  var inst = await Instance.findOne(query).populate({ path: 'schemadef', select: '-versions' })
  if (!inst) return tools.handleError(res, '资源不存在');

  var basicInfo = {
    _id: inst._id,
    code: inst.code,
    name: inst.name,
  }

  var category = await Category.findOne({ code: inst.schemadef.category })

  // 查询指定Tag的属性值
  var reqTags = req.query.tags || []
  if (!tools.isArray(reqTags)) {
    reqTags = reqTags.split(',');
  }

  var allTags = await Tag.find({ schemadef: inst.schemadef._id })
  if (allTags.length === 0) {
    return res.json({
      status: 'success', data: {
        ...basicInfo,
        tags: []
      }
    })
  }

  var tags = allTags.map(it => {
    var item = {
      code: it.code,
      name: it.name,
      attrs: []
    }

    it.attrs.forEach(attr => {
      var instAttr = inst.attrs.find(a => a.key === attr)
      var schemaAttr = [...category.attrs, ...inst.schemadef.attrs].find(b => b.field === attr)

      if (instAttr && schemaAttr) {
        item.attrs.push({ ...instAttr._doc, label: schemaAttr.name })
      }
    })

    return item
  })

  if (reqTags.length > 0) {
    tags = tags.filter(it => reqTags.includes(it.code))
  }
  return res.json({
    status: 'success', data: {
      ...basicInfo,
      tags: tags
    }
  })

});

/**
 * @swagger
 *
 * /cmdb/instances/{id}/relations:
 *   get:
 *     tags:
 *       - 实例管理
 *     summary: 查询实例关联关系
 *     description: 查询实例关联关系
 *     parameters:
 *       - name: id
 *         description: 实例ID或编码
 *         in: path
 *         required: true
 *         type: string
 *     responses:
 *       200:
 *         description: status/data结构
 */
router.get('/:id/relations', async function (req, res, next) {
  var isById = tools.isValidObjectId(req.params.id)
  var query = isById ? { _id: req.params.id } : { code: req.params.id }

  var inst = await Instance.findOne(query).populate({ path: 'schemadef', select: '-versions' })
  if (!inst) return tools.handleError(res, '资源不存在');

  RelationInstance.find({ $or: [{ src: inst._id }, { dest: inst._id }] })
    .populate('rela')
    .populate('src')
    .populate('dest')
    .exec((err, docs) => {
      if (err) return tools.handleError(res, err);
      return res.json({ status: 'success', data: docs })
    })
});

/**
 * @swagger
 *
 * /cmdb/instances/{id}/topo:
 *   get:
 *     tags:
 *       - 实例管理
 *     summary: 查询实例拓扑
 *     description: 查询实例拓扑
 *     parameters:
 *       - name: id
 *         description: 实例ID或编码
 *         in: path
 *         required: true
 *         type: string
 *     responses:
 *       200:
 *         description: status/data结构
 */
router.get('/:id/topo', async function (req, res, next) {
  var isById = tools.isValidObjectId(req.params.id)
  var query = isById ? { _id: req.params.id } : { code: req.params.id }
  var instance = await Instance.findOne(query).populate({ path: 'schemadef', select: '-versions' })
  if (!instance) {
    return tools.handleError(res, '资源不存在');
  }

  var instanceId = instance._id.toString()
  let icon = await SchemaIcon.findOne({ schemadef: instance.schemadef._id })

  var relations = await RelationInstance.find({ $or: [{ src: instance._id }, { dest: instance._id }] })
    .populate('rela')
    .populate('src')
    .populate('dest')

  var nodes = []
  var edges = []
  nodes.push({ id: instance.code, _id: instance._id, label: instance.name, icon: icon ? icon.url : '' })

  for (var i = 0; i < relations.length; i++) {
    var r = relations[i];
    var srcId = r.src._id.toString();
    var destId = r.dest._id.toString();
    if (instanceId == srcId) {
      let icon = await SchemaIcon.findOne({ schemadef: r.dest.schemadef })
      nodes.push({ id: r.dest.code, _id: r.dest._id, label: r.dest.name, icon: icon ? icon.url : '' })
      let relationType = await RelationType.findById(r.rela.type)
      var label = '<b>' + relationType.destdesc + '</b>\n'
      if (r.attrs) {
        r.attrs.forEach(attr => {
          var attrDef = r.rela.attrs.find(a => a.field == attr.key)
          label += (attrDef ? attrDef.name : attr.key) + ': ' + attr.value + '\n'
        })
      }
      edges.push({ from: r.src.code, to: r.dest.code, arrows: 'from', font: { multi: true }, label: label })
    } else {
      let icon = await SchemaIcon.findOne({ schemadef: r.src.schemadef })
      nodes.push({ id: r.src.code, _id: r.src._id, label: r.src.name, icon: icon ? icon.url : '' });
      let relationType = await RelationType.findById(r.rela.type)
      var label = '<b>' + relationType.srcdesc + '</b>\n'
      if (r.attrs) {
        r.attrs.forEach(attr => {
          var attrDef = r.rela.attrs.find(a => a.field == attr.key)
          label += (attrDef ? attrDef.name : attr.key) + ': ' + attr.value + '\n'
        })
      }
      edges.push({ from: r.src.code, to: r.dest.code, arrows: 'to', font: { multi: true }, label: label });
    }
  }

  var category = await Category.findOne({ code: instance.schemadef.category })
  var allAttrs = [...category.attrs, ...instance.schemadef.attrs]
  var foreigns = allAttrs.filter(attr => attr.dtype === 'foreign')
  for (var i = 0; i < foreigns.length; i++) {
    var attr = instance.attrs.find(it => it.key === foreigns[i].field && !!it.value)
    if (attr) {
      var foreignInst = await Instance.findOne({ code: attr.value }, { _id: 1, code: 1, name: 1, schemadef: 1 })
      let icon = await SchemaIcon.findOne({ schemadef: foreignInst.schemadef })
      nodes.push({ id: foreignInst.code, _id: foreignInst._id, label: foreignInst.name, icon: icon ? icon.url : '' })
      edges.push({ from: instance.code, to: foreignInst.code, arrows: 'to', font: { multi: true }, label: '<b>外键</b>\n' })
    }
  }

  return res.json({ status: 'success', data: { nodes, edges } })
});

/**
 * @swagger
 *
 * /cmdb/instances/{id}/relations/add:
 *   get:
 *     tags:
 *       - 实例管理
 *     summary: 查询实例关联关系
 *     description: attrs参数以对象结构提交
 *     parameters:
 *       - name: id
 *         description: 实例ID或编码
 *         in: path
 *         required: true
 *         type: string
 *       - name: src
 *         description: 源实例ID
 *         in: formData
 *         required: true
 *         type: string
 *       - name: dest
 *         description: 目标实例ID
 *         in: formData
 *         required: true
 *         type: string
 *       - name: rela
 *         description: 关联关系模型ID
 *         in: formData
 *         required: true
 *         type: string
 *       - name: attrs
 *         description: 属性
 *         in: formData
 *         required: false
 *         type: object
 *       - name: desc
 *         description: 描述
 *         in: formData
 *         required: false
 *         type: string
 *     responses:
 *       200:
 *         description: status/data结构
 */
router.put('/:id/relations/add', async function (req, res, next) {
  var isById = tools.isValidObjectId(req.params.id)
  var query = isById ? { _id: req.params.id } : { code: req.params.id }
  var instance = await Instance.findOne(query).populate({ path: 'schemadef', select: '-versions' })
  if (!instance) {
    return tools.handleError(res, '资源不存在');
  }

  // 只接受5个字段部分
  var { desc, src, dest, rela } = req.body
  var relaInst = await Relation.findById(rela)
  if (!relaInst) return tools.handleError(res, '关联关系不存在');
  var srcInst = await Instance.findById(src)
  if (!srcInst) return tools.handleError(res, '源实例不存在');
  var destInst = await Instance.findById(dest)
  if (!destInst) return tools.handleError(res, '目标实例不存在');

  var attrs = []
  relaInst.attrs.forEach(a => {
    attrs.push({ key: a.field, value: req.body.attrs[a.field] })
  })

  var data = {
    rela: relaInst._id,
    src: srcInst._id,
    dest: destInst._id,
    desc,
    attrs
  }

  // 创建新的关联关系实例（有更无增）
  await RelationInstance.updateOne({ rela: relaInst._id, src: srcInst._id, dest: destInst._id }, data, { upsert: true })
  // 拿到新增的关联关系实例
  const r = await RelationInstance.findOne({ rela: relaInst._id, src: srcInst._id, dest: destInst._id })
  // 源实例要保存关联关系实例的引用
  var srcBeforeUpdate = await Instance.findByIdAndUpdate(src, { $addToSet: { relations: r } }, { new: false });
  cache.remove(CACHE_INSTANCE, [srcBeforeUpdate._id, srcBeforeUpdate.code]);
  // var {srcVersions, ...srcData} = srcBeforeUpdate._doc
  // 目标实例要保存关联关系实例的引用
  var destBeforeUpdate = await Instance.findByIdAndUpdate(dest, { $addToSet: { relations: r } }, { new: false });
  cache.remove(CACHE_INSTANCE, [destBeforeUpdate._id, destBeforeUpdate.code]);
  // var {destVersions, ...destData} = destBeforeUpdate._doc
  // 保存目标实例变更 TODO: 暂时不保存变更
  // await Instance.findByIdAndUpdate(dest, {$push: {versions:destData}}, { new: true })
  // 保存源实例变更
  // Instance.findByIdAndUpdate(req.params.id, {$push: {versions:srcData}}, { new: true }, function (err, doc) {
  //   if (err) return tools.handleError(res, err);
  //   res.json({ status: 'success', data: doc })
  // });
  res.json({ status: 'success' })
});

/**
 * @swagger
 *
 * /cmdb/instances/{id}/relations/{rid}:
 *   delete:
 *     tags:
 *       - 实例管理
 *     summary: 删除关联关系
 *     description: 删除关联关系
 *     parameters:
 *       - name: id
 *         description: 实例ID或编码
 *         in: path
 *         required: true
 *         type: string
 *       - name: rid
 *         description: 关联关系实例ID
 *         in: formData
 *         required: true
 *         type: string
 *     responses:
 *       200:
 *         description: status/data结构
 */
router.delete('/:id/relations/:rid', async function (req, res, next) {
  const relationInstance = await RelationInstance.findById(req.params.rid).populate('src').populate('dest')
  if (!relationInstance) return tools.handleError(res, '关联关系实例不存在');
  // 移除源实例和目标实例的引用
  await Instance.updateMany({ _id: { $in: [relationInstance.src._id, relationInstance.dest._id] } }, { $pull: { relations: { $in: [relationInstance._id] } } })
  cache.remove(CACHE_INSTANCE, [relationInstance.src._id, relationInstance.src.code, relationInstance.dest._id, relationInstance.dest.code])
  // 删除关联关系实例
  RelationInstance.findByIdAndDelete(req.params.rid, function (err) {
    if (err) return tools.handleError(res, err);
    res.json({ status: 'success' })
  });
});

/**
 * @swagger
 *
 * /cmdb/instances/stats/category:
 *   get:
 *     tags:
 *       - 实例管理
 *     summary: 分类统计实例
 *     description: 分类统计实例
 *     responses:
 *       200:
 *         description: status/data结构
 */
router.get('/stats/category', async function(req, res, next) {
  var cached = await cache.exists(CACHE_INSTANCE, 'stats')
  if(!cached) {
    var data = await categoryStatus()
    // 缓存设置10分钟
    cache.set(CACHE_INSTANCE, 'stats', data, 10 * 60 * 1000)
  }

  var data = await cache.get(CACHE_INSTANCE, 'stats')
  var categories = await Category.find({code: {$in: data.map(it => it._id)}})

  data = data.map(it => {
    var cat = categories.find(c => c.code === it._id)
    if(cat) {
      return {
        code: it._id,
        name: cat.name,
        count: it.count
      }
    }
  })
  
  return res.json({ status: 'success', data: data })
});

/**
 * @swagger
 *
 * /cmdb/instances/stats/category/tree:
 *   get:
 *     tags:
 *       - 实例管理
 *     summary: 分类(2层分类树)统计实例
 *     description: 分类(2层分类树)统计实例
 *     responses:
 *       200:
 *         description: status/data结构
 */
router.get('/stats/category/tree', async function(req, res, next) {
  var cached = await cache.exists(CACHE_INSTANCE, 'stats')
  if(!cached) {
    var data = await categoryStatus()
    // 缓存设置10分钟
    cache.set(CACHE_INSTANCE, 'stats', data, 10 * 60 * 1000)
  }

  var data = await cache.get(CACHE_INSTANCE, 'stats')
  var categoryListOf2 = await Category.find({code: {$in: data.map(it => it._id)}})

  var categoryCodeListOf1 = new Set()
  categoryListOf2.forEach(it => {
    categoryCodeListOf1.add(it.parent)
  })

  var categoryListOf1 = await Category.find({code: {$in: [...categoryCodeListOf1]}})

  var result = categoryListOf1.map(c1 => {
    var item = {
      code: c1.code,
      name: c1.name,
      count: 0
    }

    var c2List = categoryListOf2.filter(c2 => c2.parent === c1.code).map(c2 => {
      var item = {
        code: c2.code,
        name: c2.name,
        count: data.find(d => d._id === c2.code).count,
      }

      return item
    })

    item.children = c2List
    item.count = c2List.reduce((acc, cur) => acc + cur.count, 0)
    return item
  })
  
  return res.json({ status: 'success', data: result })
});

/**
 * @swagger
 *
 * /cmdb/instances/stats/category/tree/{attrKey}/{attrValue}:
 *   get:
 *     tags:
 *       - 实例管理
 *     summary: 分类(2层分类树)统计实例
 *     description: 根据特定属性筛选资源后统计
 *     parameters:
 *       - name: attrKey
 *         description: 属性
 *         in: path
 *         required: true
 *         type: string
 *       - name: attrValue
 *         description: 属性值
 *         in: path
 *         required: true
 *         type: string
 *     responses:
 *       200:
 *         description: status/data结构
 */
router.get('/stats/category/tree/:attrKey/:attrValue', async function(req, res, next) {
  var reqValues =  req.params.attrValue.split(',')
  var match = {"$match":{"attrs":{"$elemMatch":{"key":req.params.attrKey,"value":{$in: reqValues}}}}}
  var data = await categoryStatus(match)
  var categoryListOf2 = await Category.find({code: {$in: data.map(it => it._id)}})

  var categoryCodeListOf1 = new Set()
  categoryListOf2.forEach(it => {
    categoryCodeListOf1.add(it.parent)
  })

  var categoryListOf1 = await Category.find({code: {$in: [...categoryCodeListOf1]}})

  var result = categoryListOf1.map(c1 => {
    var item = {
      code: c1.code,
      name: c1.name,
      count: 0
    }

    var c2List = categoryListOf2.filter(c2 => c2.parent === c1.code).map(c2 => {
      var item = {
        code: c2.code,
        name: c2.name,
        count: data.find(d => d._id === c2.code).count,
      }

      return item
    })

    item.children = c2List
    item.count = c2List.reduce((acc, cur) => acc + cur.count, 0)
    return item
  })
  
  return res.json({ status: 'success', data: result })
});

async function categoryStatus(match) {

  var pipeline = [
    {
      $lookup: {
        from: "schemadefs",
        localField: "schemadef",
        foreignField: "_id",
        as: "schemas"
      },
    },
    {
      $replaceRoot: { newRoot: { $mergeObjects: [ { $arrayElemAt: [ "$schemas", 0 ] }, "$$ROOT" ] } }
    },
    {
      $lookup: {
        from: "categories",
        localField: "category",
        foreignField: "code",
        as: "categories"
      },
    },
    { 
      $project: { 
        _id: 1,
        code:1, 
        name:1,
        category: {"$arrayElemAt": [ "$categories", 0 ]}
      } 
    },
    {
      $group: {
        _id: "$category.code",
        count: { $sum: 1 }
      }
    }
  ]

  if(match) {
    pipeline.unshift(match)
  }
  var data = await Instance.aggregate(pipeline)

  return data
}

module.exports = router;
